package com.mtsoft.mfkernel;

import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.ByteBuffer;

public class MfKernel {
    public static final String ASCII = "ISO-8859-1";
    public static final String EBCDIC = "cp1047";
    
    private static char zoneDefault = '3';
    private static char zonePositive = '3';
    private static char zoneNegative = '7';
    
    /**
     * Values are 'ASCII' or 'EBCDIC'
     * @param encoding 
     */
    public static void setEncoding(String encoding) {
        if(ASCII.equals(encoding)) {
            zoneDefault = '3';
            zonePositive = '3';
            zoneNegative = '7';
        } else if(EBCDIC.equals(encoding)) {
            zoneDefault = 'F';
            zonePositive = 'C';
            zoneNegative = 'D';
        } else {
            // TODO: error invalid encoding
        }
    }
    
    public static void loadFieldsFromByteArray(Field root, byte[] buffer) {
        if(buffer.length == 0) {
            return;
        }
        if(!root.isGroup()) {
            String display = "";
            try {
                switch(root.getStorageType()) {
                    case ALPHA:
                        display = MfKernel.loadAlpha(buffer, root.getStart(), root.getStorageLength(), root.getEncoding());
                        break;
                    case ZONED:
                    case UNSIGNED:
                        display = MfKernel.loadZonedDec(buffer, root.getStart(), root.getStorageLength(), root.getScale()).toString();
                        break;
                    case PACKED:
                        display = MfKernel.loadPackedDec(buffer, root.getStart(), root.getStorageLength(), root.getScale()).toString();
                        break;
                    case BINARY:
                        display = MfKernel.loadBinary(buffer, root.getStart(), root.getStorageLength()).toString();
                        break;
                    default:
                        throw new RuntimeException("invalid storageType: " + root.getStorageType());
                }
            } catch(Exception e) {
                int begin = Math.min(root.getStart(), buffer.length);
                int end = Math.min(root.getStorageLength(), buffer.length - begin);
                try {
                    String s = new String(buffer, begin, end, root.getEncoding());
                    s = Util.replaceCtrlPeriod(s);
                    display = s.trim();
                } catch(UnsupportedEncodingException e2) {
                    throw new RuntimeException(e2);
                }
            }
            display = Util.replaceCtrlPeriod(display);
            root.setDisplay(display.trim());
            root.setHex(Util.bytesToString(buffer, root.getStart(), root.getStorageLength()));
        }
        for(Field f : root.getChildren()) {
            loadFieldsFromByteArray(f, buffer);
        }
    }

    public static String loadAlpha(byte[] buffer, int start, int length, String encoding) {
        String retVal = "";
        // if the start is greater than length return ""
        if(start > buffer.length) {
            return retVal;
        }
        // lenght is min of length and start - buffer
        length = Math.min(length, buffer.length - start);
        try {
            retVal = new String(buffer, start, length, encoding);
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
        return retVal;
    }
    
    public static void storeAlpha(String value, byte[] buffer, int start, int length, String encoding) {
        String alpha = formatAlpha(value, length);
        try {
            int i = 0;
            for(byte b : alpha.getBytes(encoding)) {
                buffer[i] = b;
                i++;
            }
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * Stores a BigDecimal in a byte array in mainframe zoned decimal format.
     * @param value
     * @param buffer
     * @param start
     * @param length
     * @param scale
     */
    public static void storeZonedDec(BigDecimal value, byte[] buffer, int start, int length, int scale) {
        String num = formatDec(value, length, scale);
        if(num.length() > length) {
            String msg = String.format("Data truncation: %s is length %d, should be length %d",
                    num, num.length(), length);
            throw new RuntimeException(msg);
        }
        char[] chars = new char[length * 2];
        int i = 0;
        while(i < chars.length) {
            chars[i] = zoneDefault;
            chars[i+1] = num.charAt(i/2);
            i += 2;
        }
        if(value.signum() < 0) {
            chars[chars.length - 2] = zoneNegative;
        } else {
            chars[chars.length - 2] = zonePositive;
        }
        collapse(chars, buffer, start, length);
    }

    /**
     * Stores a BigDecimal in a byte array in mainframe packed decimal format.
     * @param value
     * @param buffer
     * @param start
     * @param length
     * @param scale
     */
    public static void storePackedDec(BigDecimal value, byte[] buffer, int start, int length, int scale) {
        int charLen = (2 * length) - 1;
        String num = formatDec(value, charLen, scale);
        if(num.length() > charLen) {
            String msg = String.format("Data truncation: %s is length %d, should be length %d",
                    num, num.length(), charLen);
            throw new RuntimeException(msg);
        }
        char[] chars = new char[2 * length];
        int i = 0;
        while(i < charLen) {
            chars[i] = num.charAt(i);
            i++;
        }
        if(value.signum() < 0) {
            chars[chars.length - 1] = zoneNegative;
        } else {
            chars[chars.length - 1] = zonePositive;
        }
        collapse(chars, buffer, start, length);
    }

    /**
     * Creates a BigDecimal from a bytes formatted as mainframe zoned decimal.
     * @param buffer
     * @param start
     * @param length
     * @param scale
     * @return
     */
    public static BigDecimal loadZonedDec(byte[] buffer, int start, int length, int scale) {
        char[] chars = explode(buffer, start, length);
        StringBuilder num = new StringBuilder();
        int i = 1;
        while(i < chars.length) {
            num.append(chars[i]);
            i += 2;
        }
        if(isNegChar(chars[chars.length - 2])) {
            num.insert(0, '-');
        }
        if(scale > 0) {
            num.insert(num.length() - scale, '.');
        }
        return new BigDecimal(num.toString());
    }

    /**
     * Creates a BigDecimal from a bytes formatted as mainframe packed decimal.
     * @param buffer
     * @param start
     * @param length
     * @param scale
     * @return
     */
    public static BigDecimal loadPackedDec(byte[] buffer, int start, int length, int scale) {
        char[] chars = explode(buffer, start, length);
        StringBuilder num = new StringBuilder();
        int i = 0;
        while(i < chars.length - 1) {
            num.append(chars[i]);
            i ++;
        }
        if(isNegChar(chars[chars.length - 1])) {
            num.insert(0, '-');
        }
        if(scale > 0) {
            num.insert(num.length() - scale, '.');
        }
        return new BigDecimal(num.toString());
    }

    /**
     * Loads a long from byte array for a given offset, length of bytes.
     * 
     * Note ignoring sign because comp n fields shouldn't use all bytes anyway
     * 
     * @param buffer
     * @param start
     * @param length
     * @param signed
     * @return
     */
    public static Long loadBinary(byte[] buffer, int start, int length) {
        byte[] bytes;
        if(buffer[start] < 0) {
            bytes = new byte[] {-1, -1, -1, -1, -1, -1, -1, -1};
        } else {
            bytes = new byte[] {0, 0, 0, 0, 0, 0, 0, 0};
        }
        System.arraycopy(buffer, start, bytes, bytes.length - length, length);
        return ByteBuffer.wrap(bytes).getLong();
    }

    /**
     * Stores part of a long value into a byte array given an offset and length.
     * @param value
     * @param buffer
     * @param start
     * @param length
     * @param signed
     */
    public static void storeBinary(Long value, byte[] buffer, int start, int length, boolean signed) {
        int LENGTH = 8;
        ByteBuffer bb = ByteBuffer.allocate(LENGTH);
        bb.putLong(value);
        int ptr = LENGTH - length;
        System.arraycopy(bb.array(), ptr, buffer, start, length);
    }

    /**
     * Stores a char array in a byte array by putting every to chars into one byte.
     * @param chars
     * @param buffer
     * @param start
     * @param length
     */
    public static void collapse(char[] chars, byte[] buffer, int start, int length) {
        int i = 0;
        while(i < chars.length) {
            byte b1 = charToNibble(chars[i]);
            byte b2 = charToNibble(chars[i+1]);
            b1 = (byte)(b1 << 4);
            b1 = (byte)(b1 | b2);
            buffer[start + i/2] = b1;
            i += 2;
        }
    }

    /**
     * Expands each byte from a byte array into two chars, one for each nibble.
     * @param buffer
     * @param start
     * @param length
     * @return
     */
    public static char[] explode(byte[] buffer, int start, int length) {
        char[] nibbles = new char[length * 2];
        int nptr = 0;
        for(int i = start; i < start + length; i++) {
            byte b = buffer[i];
            byte b2 = (byte)(b & 0x0f);
            byte b1 = (byte)((b & 0xff) >>> 4);
            nibbles[nptr] = nibbleToChar(b1);
            nibbles[nptr+1] = nibbleToChar(b2);
            nptr += 2;
        }
        return nibbles;
    }

    /**
     * Formats a BigDecimal have all required leading and trailing zeroes, and
     * removes the decimal point and sign.
     * @param value
     * @param length
     * @param scale
     * @return
     */
    public static String formatDec(BigDecimal value, int length, int scale) {
        value = value.setScale(scale, RoundingMode.HALF_UP);
        value = value.abs();
        StringBuilder num = new StringBuilder(value.toPlainString());
        if(scale > 0) {
            num.deleteCharAt(num.indexOf("."));
        }
        while(num.length() < length) {
            num.insert(0, '0');
        }
        return num.toString();
    }
    
    public static String formatAlpha(String value, int length) {
        StringBuilder sb = new StringBuilder(value);
        while(sb.length() < length) {
            sb.append(" ");
        }
        return sb.toString();
    }

    public static byte charToNibble(char c) {
        switch(c) {
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                return (byte)(c - 48);
            case 'A':
            case 'B':
            case 'C':
            case 'D':
            case 'E':
            case 'F':
                return (byte)(c - 55);
            default:
                return 0;
        }
    }

    public static char nibbleToChar(byte b) {
        switch(b) {
            case 0:
            case 1:
            case 2:
            case 3:
            case 4:
            case 5:
            case 6:
            case 7:
            case 8:
            case 9:
                return (char)(b + 48);
            case 10:
            case 11:
            case 12:
            case 13:
            case 14:
            case 15:
                return (char)(b + 55);
            default:
                return 0;
        }
    }

    public static boolean isNegChar(char c) {
        return c == zoneNegative;
    }
    
    private static void printNibbles(char[] chars) {
        System.out.print("chars:");
        for(char c : chars) {
            System.out.print(c);
        }
        System.out.println(":");
    }
}
